import assert from 'nanoassert'
import utf8d from '@3-/utf8/utf8d.js'
import utf8e from '@3-/utf8/utf8e.js'

export const wireTypes = {
  VARINT: 0,
  BYTES: 2,
  FIXED64: 1,
  FIXED32: 5
}

export const varint = {
  encode (
    int,
    buf = alloc(this, int),
    byteOffset = 0
  ) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    let o = byteOffset
    let n = BigInt(int)

    while (n >= 128n) {
      buf[o++] = Number((n & 0xffn) | 0b1000_0000n)
      n >>= 7n
    }

    buf[o++] = Number(n)

    this.encode.bytes = o - byteOffset
    return buf.subarray(byteOffset, o)
  },
  encodeOversize (int, len, buf, byteOffset = 0) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    assert(len >= this.encodingLength(int), 'len does not fit int')
    assert(buf.byteLength - byteOffset >= len, 'buf does not fit len')
    let o = byteOffset
    const end = byteOffset + len - 1

    let n = BigInt(int)

    while (o < end) {
      buf[o++] = Number((n & 0xffn) | 0b1000_0000n)
      n >>= 7n
    }

    buf[o++] = Number(n)

    this.encodeOversize.bytes = o - byteOffset
    return buf.subarray(byteOffset, o)
  },
  encodingLength (int) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    if (int <= 0xffff_ffff) return (9 * (32 - Math.clz32(Number(int))) + 64) / 64 | 0
    const high = Number(BigInt(int) >> 32n)
    return (9 * (32 - Math.clz32(high) + 32) + 64) / 64 | 0
  },
  MIN_VALUE: 0n,
  MAX_VALUE: (1n << 64n) - 1n
}

export const bytes = {
  encode (src, buf = alloc(this, src), byteOffset = 0) {
    let o = byteOffset
    varint.encode(src.byteLength, buf, o)
    o += varint.encode.bytes
    buf.set(src, o)
    o += src.byteLength
    this.encode.bytes = o - byteOffset
    return buf.subarray(byteOffset, o)
  },
  encodingLength (src) {
    return varint.encodingLength(src.byteLength) + src.byteLength
  }
}

export const tag = {
  encode (
    fieldNumber,
    wireType,
    buf = alloc(this, fieldNumber),
    byteOffset = 0
  ) {
    assert(fieldNumber > 0, 'fieldNumber must be greater than 0')
    assert(fieldNumber <= tag.MAX_VALUE, 'fieldNumber exceeds MAX_VALUE')
    const int = BigInt.asUintN(32, BigInt(fieldNumber)) << 3n | BigInt(wireType)
    varint.encode(int, buf, byteOffset)
    this.encode.bytes = varint.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength (fieldNumber) {
    assert(fieldNumber > 0, 'fieldNumber must be greater than 0')
    assert(fieldNumber <= tag.MAX_VALUE, 'fieldNumber exceeds MAX_VALUE')

    return (9 * (32 - Math.clz32(Number(fieldNumber) << 3)) + 64) / 64 | 0
  },
  MIN_VALUE: 1n,
  MAX_VALUE: (1n << 29n) - 1n
}

export const string = {
  encode (str, buf = alloc(this, str), byteOffset = 0) {
    assert(typeof str === 'string')
    const src = utf8e(str)
    bytes.encode(src, buf, byteOffset)
    this.encode.bytes = bytes.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength (str) {
    const len = [...str].length

    return varint.encodingLength(len) + len
  }
}

export const uint64 = {
  encode (uint, buf = alloc(this, uint), byteOffset = 0) {
    assert(uint >= this.MIN_VALUE, 'uint exceeds MIN_VALUE')
    assert(uint <= this.MAX_VALUE, 'uint exceeds MAX_VALUE')
    const biguint = BigInt(uint)
    varint.encode(BigInt.asUintN(64, biguint), buf, byteOffset)
    this.encode.bytes = varint.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength (uint) {
    assert(uint >= this.MIN_VALUE, 'uint exceeds MIN_VALUE')
    assert(uint <= this.MAX_VALUE, 'uint exceeds MAX_VALUE')
    return varint.encodingLength(uint)
  },
  MIN_VALUE: varint.MIN_VALUE,
  MAX_VALUE: varint.MAX_VALUE
}

export const int64 = {
  encode (int, buf = alloc(this, int), byteOffset = 0) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    const bigint = BigInt(int)
    varint.encode(BigInt.asUintN(64, bigint), buf, byteOffset)
    this.encode.bytes = varint.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength (int) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    return varint.encodingLength(BigInt.asUintN(64, BigInt(int)))
  },
  MIN_VALUE: -(1n << 63n),
  MAX_VALUE: (1n << 63n) - 1n
}

export const sint64 = {
  encode (int, buf = alloc(this, int), byteOffset = 0) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    const bigint = BigInt(int)
    varint.encode(BigInt.asUintN(64, (bigint << 1n) ^ (bigint >> 63n)), buf, byteOffset)
    this.encode.bytes = varint.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength (int) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    const bigint = BigInt(int)
    return varint.encodingLength((bigint << 1n) ^ (bigint >> 63n))
  },
  MIN_VALUE: -(1n << 63n),
  MAX_VALUE: (1n << 63n) - 1n
}

export const uint32 = {
  encode (uint, buf = alloc(this, uint), byteOffset = 0) {
    assert(uint >= this.MIN_VALUE, 'uint exceeds MIN_VALUE')
    assert(uint <= this.MAX_VALUE, 'uint exceeds MAX_VALUE')
    const bigint = BigInt(uint)
    varint.encode(BigInt.asUintN(32, bigint), buf, byteOffset)
    this.encode.bytes = varint.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength (uint) {
    assert(uint >= this.MIN_VALUE, 'uint exceeds MIN_VALUE')
    assert(uint <= this.MAX_VALUE, 'uint exceeds MAX_VALUE')
    return varint.encodingLength(uint)
  },
  MIN_VALUE: 0,
  MAX_VALUE: (1n << 32n) - 1n
}

export const int32 = {
  encode (int, buf = alloc(this, int), byteOffset = 0) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    const bigint = BigInt(int)
    varint.encode(BigInt.asUintN(32, bigint), buf, byteOffset)
    this.encode.bytes = varint.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength (int) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    return varint.encodingLength(BigInt.asUintN(32, BigInt(int)))
  },
  MIN_VALUE: -(1n << 31n),
  MAX_VALUE: (1n << 31n) - 1n
}

export const sint32 = {
  encode (int, buf = alloc(this, int), byteOffset = 0) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    const bigint = BigInt(int)
    varint.encode((bigint << 1n) ^ (bigint >> 31n), buf, byteOffset)
    this.encode.bytes = varint.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength (int) {
    assert(int >= this.MIN_VALUE, 'int exceeds MIN_VALUE')
    assert(int <= this.MAX_VALUE, 'int exceeds MAX_VALUE')
    const bigint = BigInt(int)
    return varint.encodingLength((bigint << 1n) ^ (bigint >> 31n))
  },
  MIN_VALUE: -(1n << 31n),
  MAX_VALUE: (1n << 31n) - 1n
}

export const bool = {
  encode (val, buf = alloc(this), byteOffset = 0) {
    varint.encode(val === true ? 1 : 0, buf, byteOffset)
    this.encode.bytes = varint.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength () {
    return 1
  }
}

export const double = {
  encode (val, buf = alloc(this), byteOffset = 0) {
    _view(buf).setFloat64(byteOffset, val, true)
    this.encode.bytes = 8
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength () {
    return 8
  }
}

export const float = {
  encode (val, buf = alloc(this), byteOffset = 0) {
    _view(buf).setFloat32(byteOffset, val, true)
    this.encode.bytes = 4
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength () {
    return 4
  }
}

function _view (bytes) {
  return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
}

export const enum_ = {
  encode (en, buf = alloc(this, en), byteOffset = 0) {
    assert(en >= enum_.MIN_VALUE, 'enum value exceeds MIN_VALUE')
    assert(en <= enum_.MAX_VALUE, 'enum value exceeds MAX_VALUE')

    en = Number(en) >>> 0 // cast to uint32 for varint encoding
    varint.encode(en, buf, byteOffset)
    this.encode.bytes = varint.encode.bytes
    return buf.subarray(byteOffset, byteOffset + this.encode.bytes)
  },
  encodingLength (en) {
    assert(en >= enum_.MIN_VALUE, 'enum value exceeds MIN_VALUE')
    assert(en <= enum_.MAX_VALUE, 'enum value exceeds MAX_VALUE')
    return varint.encodingLength(Number(en) >>> 0)
  },
  MIN_VALUE: -(1n << 31n),
  MAX_VALUE: (1n << 31n) - 1n
}


export function alloc (ctx, ...data) {
  return new Uint8Array(ctx.encodingLength(...data))
}

export const utf8 = {
  encode (buf) { return utf8d(buf) },
  decode (str) { return utf8e(str) }
}

// module.exports = {
//   wireTypes,
//   varint,
//   bytes,
//   tag,
//   string,
//   int64,
//   uint64,
//   sint64,
//   int32,
//   uint32,
//   sint32,
//   bool,
//   double,
//   float,
//   enum_,
//   utf8,
//   alloc
// }
